package cn.pugle.quartz.cluster.annotation;

import org.apache.commons.lang3.StringUtils;
import org.quartz.CronScheduleBuilder;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SimpleScheduleBuilder;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.EmbeddedValueResolverAware;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor;
import org.springframework.scheduling.quartz.SchedulerAccessor;
import org.springframework.util.Assert;
import org.springframework.util.MethodInvoker;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringValueResolver;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;

import cn.pugle.quartz.cluster.MethodInvokingJob;

import static cn.pugle.quartz.cluster.MethodInvokingJob.JOB_INVOKER_BEAN_NAME_KEY;
import static org.quartz.DateBuilder.IntervalUnit.MILLISECOND;
import static org.quartz.DateBuilder.futureDate;

/**
 * 处理使用{@link QuartzScheduled}注解的方法, 使用quartzScheduler调度。 <br>
 * 不想支持一个方法注多次，删掉了 <br>
 * setOverwriteExistingJobs(false); 不覆盖已存在的job <br>
 * 放入scheduler的jobDetail和trigger都以类的Name做group，方法名做名 <br>
 *
 * copy from {@link ScheduledAnnotationBeanPostProcessor} and {@link } <br>
 * Created by tzp on 16/11/28.
 */
public class QuartzScheduledAnnotationBeanPostProcessor extends SchedulerAccessor
        implements BeanPostProcessor, Ordered, ApplicationContextAware,EmbeddedValueResolverAware,
        SmartInitializingSingleton, ApplicationListener<ContextRefreshedEvent>, DisposableBean {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    private Scheduler scheduler;
    private ApplicationContext applicationContext;
    private StringValueResolver embeddedValueResolver;

    private final Set<Class<?>> nonAnnotatedClasses =
            Collections.newSetFromMap(new ConcurrentHashMap<Class<?>, Boolean>(64));

    private ArrayList<Trigger> triggers = new ArrayList<Trigger>();
    private ArrayList<JobDetail> details = new ArrayList<JobDetail>();

    /**
     * 从刚实例化的bean中找到有{@link QuartzScheduled}注解的方法
     */
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        Class<?> targetClass = AopUtils.getTargetClass(bean);
        if (!this.nonAnnotatedClasses.contains(targetClass)) {
            Map<Method, QuartzScheduled> annotatedMethods = MethodIntrospector.selectMethods(targetClass,
                    new MethodIntrospector.MetadataLookup<QuartzScheduled>() {
                        @Override
                        public QuartzScheduled inspect(Method method) {
                            return AnnotationUtils.getAnnotation(method, QuartzScheduled.class);
                        }
                    });
            if (annotatedMethods.isEmpty()) {
                this.nonAnnotatedClasses.add(targetClass);
                if (logger.isTraceEnabled()) {
                    logger.trace("No @QuartzScheduled annotations found on bean class: " + bean.getClass());
                }
            } else {
                // 找到被注解的方法
                for (Map.Entry<Method, QuartzScheduled> entry : annotatedMethods.entrySet()) {
                    processQuartzScheduledMethod(entry.getKey(), entry.getValue(), bean);
                }
                logger.debug(annotatedMethods.size() + " @QuartzScheduled methods processed on bean '" + beanName +
                        "': " + annotatedMethods);

            }
        }
        return bean;
    }

    /**
     * 处理找到的有{@link QuartzScheduled}注解的方法。<br>
     * 生成一个jobDetail 一个JobTrigger 一个MethodInvoker存下来。<br>
     */
    private void processQuartzScheduledMethod(Method method, QuartzScheduled scheduled, Object bean) {
        String methodName = method.getName();
        String className = method.getDeclaringClass().getName();
        try {
            Assert.isTrue(void.class == method.getReturnType(),
                    "Only void-returning methods may be annotated with @Scheduled");
            Assert.isTrue(method.getParameterTypes().length == 0,
                    "Only no-arg methods may be annotated with @Scheduled");

            if (AopUtils.isJdkDynamicProxy(bean)) {
                try {
                    // Found a @Scheduled method on the target class for this JDK proxy ->
                    // is it also present on the proxy itself?
                    method = bean.getClass().getMethod(methodName, method.getParameterTypes());
                } catch (SecurityException ex) {
                    ReflectionUtils.handleReflectionException(ex);
                } catch (NoSuchMethodException ex) {
                    throw new IllegalStateException(String.format(
                            "@Scheduled method '%s' found on bean target class '%s' but not " +
                                    "found in any interface(s) for a dynamic proxy. Either pull the " +
                                    "method up to a declared interface or switch to subclass (CGLIB) " +
                                    "proxies by setting proxy-target-class/proxyTargetClass to 'true'.",
                            methodName, className));
                }
            } else if (AopUtils.isCglibProxy(bean)) {
                // Common problem: private methods end up in the proxy instance, not getting delegated.
                if (Modifier.isPrivate(method.getModifiers())) {
                    throw new IllegalStateException(String.format(
                            "@Scheduled method '%s' found on CGLIB proxy for target class '%s' but cannot " +
                                    "be delegated to target bean. Switch its visibility to package or protected.",
                            methodName, className));
                }
            }

            String invokerName = methodName + "@" + className;
            MethodInvoker methodInvoker = new MethodInvoker();
            methodInvoker.setTargetObject(bean);
            methodInvoker.setTargetMethod(method.getName());
            methodInvoker.prepare();

            JobDetail jobDetail = JobBuilder.newJob(MethodInvokingJob.class)
                    .withIdentity(methodName, className)
                    .storeDurably(true)
                    .usingJobData(JOB_INVOKER_BEAN_NAME_KEY, invokerName)
                    .build();

            Trigger jobTrigger = null;

            // Determine initial delay
            int initialDelay = scheduled.initialDelay();
            if (initialDelay < 0) {
                initialDelay = 0;
            }

            // Check cron expression。
            String cron = scheduled.cron();
            if (StringUtils.isNotEmpty(cron)) {
                if (this.embeddedValueResolver != null) {
                    cron = this.embeddedValueResolver.resolveStringValue(cron);
                }
                String zone = scheduled.zone();
                if (this.embeddedValueResolver != null) {
                    zone = this.embeddedValueResolver.resolveStringValue(zone);
                }
                TimeZone timeZone;
                if (StringUtils.isNotEmpty(zone)) {
                    timeZone = TimeZone.getTimeZone(zone);
                } else {
                    timeZone = TimeZone.getDefault();
                }
                jobTrigger = TriggerBuilder.newTrigger()
                        .withIdentity(methodName, className)
                        .forJob(jobDetail)
                        .withSchedule(CronScheduleBuilder.cronSchedule(cron)
                                .inTimeZone(timeZone))
                        .startAt(futureDate(initialDelay, MILLISECOND))
                        .build();
            }

            // Check fixed rate
            int interval = scheduled.interval();
            if (interval >= 0) {
                jobTrigger = TriggerBuilder.newTrigger()
                        .withIdentity(methodName, className)
                        .forJob(jobDetail)
                        .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                                .withIntervalInMilliseconds(interval)
                                .repeatForever())
                        .startAt(futureDate(initialDelay, MILLISECOND))
                        .build();
            }

            if (null == jobTrigger) {
                throw new IllegalStateException("Exactly one of the 'cron' or 'interval(String)' attributes is required");
            }


            MethodInvokingJob.InvokerRepository.addInvoker(invokerName, methodInvoker);
            details.add(jobDetail);
            triggers.add(jobTrigger);
        } catch (Exception e) {
            logger.error("Error while process @QuartzScheduled method " + methodName + className, e);
        }
    }

    /**
     * 向scheduler注册job <br>
     * 在{@link #afterSingletonsInstantiated}或{@link #onApplicationEvent} 中调用。
     */
    private void finishRegistration() {
        try {
            setOverwriteExistingJobs(false);
            setTriggers(triggers.toArray(new Trigger[triggers.size()]));
            setJobDetails(details.toArray(new JobDetail[details.size()]));
            registerJobsAndTriggers();
        } catch (Exception e) {
            logger.error("Error while registerJobsAndTriggers ", e);
        }
    }

    // ----  一些接口的`普通`实现

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    public void setScheduler(Scheduler scheduler) {
        this.scheduler = scheduler;
    }

    @Override
    protected Scheduler getScheduler() {
        return scheduler;
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }

    /**
     * Setting an {@link ApplicationContext} is optional: If set, registered
     * tasks will be activated in the {@link ContextRefreshedEvent} phase;
     * if not set, it will happen at {@link #afterSingletonsInstantiated} time.
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }

    @Override
    public void afterSingletonsInstantiated() {
        if (this.applicationContext == null) {
            // Not running in an ApplicationContext -> register tasks early...
            finishRegistration();
        }
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (event.getApplicationContext() == this.applicationContext) {
            // Running in an ApplicationContext -> register tasks this late...
            // giving other ContextRefreshedEvent listeners a chance to perform
            // their work at the same time (e.g. Spring Batch's job registration).
            finishRegistration();
        }
    }

    @Override
    public void destroy() throws Exception {

    }

    @Override
    public void setEmbeddedValueResolver(StringValueResolver embeddedValueResolver) {
        this.embeddedValueResolver = embeddedValueResolver;
    }
}
